From 835d38b078f805b3600782c346bdb7505da950f1 Mon Sep 17 00:00:00 2001 From: electricface Date: Mon, 29 Jun 2026 16:03:23 +0800 Subject: [PATCH] repo-commit: preserve existing object inode when staging to objects/ When ostree commit --consume is used and an object with the same checksum already exists in objects/, rename_pending_loose_objects() was unconditionally renaming the staging copy over it. On Linux, renameat(2) atomically replaces the destination for two regular files, silently changing the inode of the existing repo object. Fix this by checking whether the object already exists in objects/ before renaming. If it does, the content is identical by definition (the object store is content-addressed by SHA256), so we can simply unlink the staging copy and keep the existing object with its original inode. Exception: .commitmeta objects are keyed by commit checksum rather than their own content, so they can be updated in place (e.g. when GPG signatures are added or deleted via ostree gpg-sign). These are always renamed unconditionally. --- src/libostree/ostree-repo-commit.c | 31 +++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/libostree/ostree-repo-commit.c b/src/libostree/ostree-repo-commit.c index 36240fae..b7773e1d 100644 --- a/src/libostree/ostree-repo-commit.c +++ b/src/libostree/ostree-repo-commit.c @@ -1801,9 +1801,34 @@ rename_pending_loose_objects (OstreeRepo *self, GCancellable *cancellable, GErro cancellable, error)) return FALSE; - if (!glnx_renameat (child_dfd_iter.fd, loose_objpath + 3, self->objects_dir_fd, - loose_objpath, error)) - return FALSE; + /* For content-addressed objects, use RENAME_NOREPLACE so that an + * existing object keeps its inode. If the destination already + * exists (EEXIST) the content is identical by definition, so just + * drop the staging copy. + * + * Exception: .commitmeta objects are keyed by commit checksum, not + * by their own content, so they can be updated in place (e.g. when + * GPG signatures are added or deleted). Always overwrite those. + */ + if (g_str_has_suffix (child_dent->d_name, ".commitmeta")) + { + if (!glnx_renameat (child_dfd_iter.fd, loose_objpath + 3, self->objects_dir_fd, + loose_objpath, error)) + return FALSE; + } + else if (glnx_renameat2_noreplace (child_dfd_iter.fd, loose_objpath + 3, + self->objects_dir_fd, loose_objpath) + < 0) + { + if (errno == EEXIST) + { + /* Object already present with same content; drop staging copy */ + if (!glnx_unlinkat (child_dfd_iter.fd, loose_objpath + 3, 0, error)) + return FALSE; + } + else + return glnx_throw_errno_prefix (error, "renameat2(noreplace, %s)", loose_objpath); + } } } -- 2.39.5